import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class LazyLoaderService {
  private resources = new Map<string, {
    loadStatus: 'notLoaded' | 'loaded' | 'loadError',
    callbacks: { callback: Function, resolve: Function, reject: Function }[],
    corePaths: string[] | string[][],
    subPaths?: string[] | string[][],
    error?: any,
    helper?: any,
  }>();

  register(key: string, options: { corePaths: string[] | string[][], subPaths?: string[] | string[][], helper?: any }) {
    if (!this.resources.get(key)) {
      this.resources.set(key, { loadStatus: 'notLoaded', callbacks: [], ...options });
    } else {
      // tslint:disable-next-line:no-console
      console.warn('lazy resource have registered key:' + key);
    }
  }

  load(key: string, callback?: Function): Promise<any> {
    const resource = this.resources.get(key);
    if (!resource) {
      throw new Error('lazy resource not yet registered key:' + key);
    }

    return new Promise((resolve, reject) => {
      if (resource.loadStatus === 'loaded') {
        if (callback) {
          callback(resource.helper);
        }
        resolve(resource.helper);
      } else if (resource.loadStatus === 'loadError') {
        reject(resource.error);
      } else {
        if (resource.loadStatus === 'notLoaded') {
          this._load(resource);
        }
        resource.callbacks.push({ callback, resolve, reject });
      }
    });
  }

  private _load(resource) {
    resource.loadStatus = 'loading';
    this.loadResources(resource.corePaths, resource.subPaths).then(() => {
      resource.loadStatus = 'loaded';
      resource.callbacks.forEach(({ resolve, callback }) => {
        if (callback) {
          callback(resource.helper);
        }
        resolve(resource.helper);
      });
    }).catch((e) => {
      resource.loadStatus = 'loadError';
      resource.callbacks.forEach(({ reject, callback }) => {
        if (callback) {
          resource.error = e;
          callback(resource.error);
        }
        reject(e);
      });
    });
  }

  private loadResources(corePaths: any[], subPaths: any[] = []): Promise<any> {
    subPaths.forEach((path) => {
      this._loadResources(path);
    });

    return Promise.all(corePaths.map((path: string | string[]) => {
      return this._loadResources(path);
    }));
  }

  private _loadResources(path: string | string[]) {
    let promise = Promise.resolve();
    if (!Array.isArray(path)) {
      path = [path];
    }

    for (let i = 0, method = 'loadScript'; i < path.length; i++) {
      const p = path[i];
      if (p.endsWith('.css')) {
        method = 'loadStyle';
      }
      promise = promise.then(() => this[method](p));
    }

    return promise;
  }

  private loadStyle(path: string) {
    return new Promise((resolve, reject) => {
      const link = document.createElement('link');
      link.rel = 'stylesheet';
      link.href = path;
      link.addEventListener('load', () => {
        resolve(null);
      });
      link.addEventListener('error', (e) => {
        reject(e);
      });
      document.head.appendChild(link);
    });
  }

  private loadScript(path: string) {
    return new Promise((resolve, reject) => {
      const script = document.createElement('script');
      script.async = true;
      script.src = path;
      script.addEventListener('load', () => {
        resolve(null);
      });
      script.addEventListener('error', (e) => {
        reject(e);
      });
      document.head.appendChild(script);
    });
  }

}
